using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Godot;

namespace Arcane.Source.Scripting
{
    public class ParseException : Exception
    {
        public ParseException() : base() { }
        public ParseException(string msg) : base(msg) { }
    }

    public abstract partial class Ast
    {
        public static Ast Parse(string src)
        {
            var span = src.AsSpan();
            List<Ast> asts = [];
            while (span.Length > 0)
            {
                asts.Add(ParseAst(ref span));
                while (span.Length > 0 && char.IsWhiteSpace(span[0])) span = span[1..];
            }
            return asts.Count > 0 ? Comp([.. asts]) : Val(new());
        }

        public static Ast ParseAst(ref ReadOnlySpan<char> src)
        {
            while (src.Length > 0 && char.IsWhiteSpace(src[0])) src = src[1..];

            if (src.Length <= 0 || src[0].IsAnyCompoundEnd()) throw new ParseException();

            return src[0] switch
            {
                '#' => ParseBool(ref src),
                '"' => ParseString(ref src),
                '\'' => ParseQuote(ref src),
                '-' => ParseNeg(ref src),
                char c when c.IsCompoundBegin() => ParseCompound(ref src),
                char c when char.IsAsciiDigit(c) => ParseNumber(ref src),
                ':' => ParseKVPair(ref src),
                _ => ParseIdent(ref src),
            };
        }

        private static Ast ParseNeg(ref ReadOnlySpan<char> src)
        {
            if (src[0] != '-') throw new ParseException();
            src = src[1..];
            if (src.Length == 0 || src[0].IsTerminal())
                return Id("-");
            else if (char.IsAsciiDigit(src[0]))
                return ((Value)ParseNumber(ref src)).Neg();
            else
                throw new ParseException();
        }

        private static Quoted ParseQuote(ref ReadOnlySpan<char> src)
        {
            if (src[0] != '\'') throw new ParseException();
            src = src[1..];
            return Quote(ParseAst(ref src));
        }

        private static Ast ParseCompound(ref ReadOnlySpan<char> src)
        {
            if (!src[0].IsCompoundBegin()) throw new ParseException($"Expect {{begin}} got {src[0]}");
            var begin = src[0];
            src = src[1..];

            List<Ast> asts = [];
            if (src.Length == 0) throw new ParseException();

            while (src.Length > 0)
            {
                asts.Add(ParseAst(ref src));

                while (char.IsWhiteSpace(src[0])) src = src[1..];
                if (src[0].IsCompoundEnd(begin))
                {
                    src = src[1..];
                    return asts.Count > 0 ? Comp([.. asts]) : Val(new());
                }

                if (src.Length == 0) throw new ParseException();
            }

            throw new ParseException();
        }

        private static Value ParseBool(ref ReadOnlySpan<char> src)
        {
            if (src[0] != '#') throw new ParseException();
            src = src[1..];

            bool ans = src[0] switch
            {
                't' => true,
                'f' => false,
                _ => throw new ParseException(),
            };

            src = src[1..];
            return Val(ans);
        }

        #region Number Parsing
        public static Ast ParseNumber(ref ReadOnlySpan<char> src)
        {
            switch (src[0])
            {
                case '0':
                    src = src[1..];
                    if (src.Length == 0)
                        return Val(0);

                    switch (src[0])
                    {
                        case 'x':
                            src = src[1..];
                            return ParseHex(ref src);
                        case 'o':
                            src = src[1..];
                            return ParseOct(ref src);
                        case 'b':
                            src = src[1..];
                            return ParseBinary(ref src);
                        case '.':
                            src = src[1..];
                            return ParseFloat(ref src, 0);
                        case char c when char.IsAsciiDigit(c):
                            return ParseNumber(ref src);
                        case char c when c.IsTerminal():
                            return Val(0);
                    }
                    throw new ParseException();
                case char c when char.IsAsciiDigit(c):
                    long ans = 0;
                    while (src.Length > 0 && !src[0].IsTerminal())
                    {
                        if (src[0] == '.')
                        {
                            src = src[1..];
                            return ParseFloat(ref src, ans);
                        }
                        if (char.ToLower(src[0]) == 'e')
                        {
                            src = src[1..];
                            return ParseSciFloat(ref src, ans);
                        }

                        if (!char.IsAsciiDigit(src[0]))
                            throw new ParseException();

                        int digit = src[0] - '0';
                        ans *= 10;
                        ans += digit;

                        src = src[1..];
                    }
                    return Val(ans);
                default:
                    GD.PrintErr($"Failed parsing {src[0]}");
                    throw new ParseException();
            }
        }

        private static Value ParseHex(ref ReadOnlySpan<char> src)
        {
            long ans = 0;
            while (src.Length > 0 && !src[0].IsTerminal())
            {
                if (!char.IsAsciiHexDigit(src[0]))
                    throw new ParseException();

                int digit = char.ToLower(src[0]) switch
                {
                    char c when char.IsAsciiDigit(c) => c - '0',
                    char c => c - 'a' + 10,
                };
                ans *= 16;
                ans += digit;

                src = src[1..];
            }
            return Val(ans);
        }

        private static Value ParseOct(ref ReadOnlySpan<char> src)
        {
            long ans = 0;
            while (src.Length > 0 && !src[0].IsTerminal())
            {
                if (!char.IsAsciiDigit(src[0]))
                    throw new ParseException();

                int digit = src[0] - '0';

                if (digit > 7)
                    throw new ParseException();

                ans *= 8;
                ans += digit;

                src = src[1..];
            }
            return Val(ans);
        }

        private static Value ParseBinary(ref ReadOnlySpan<char> src)
        {
            long ans = 0;
            while (src.Length > 0 && !src[0].IsTerminal())
            {
                switch (src[0])
                {
                    case '0':
                        break;
                    case '1':
                        ans &= 1;
                        break;
                    default:
                        throw new ParseException();
                }
                ans <<= 1;
            }
            return Val(ans);
        }

        private static Value ParseFloat(ref ReadOnlySpan<char> src, long integer)
        {
            double ans = 0;
            double pos = 0.1;
            while (src.Length > 0 && !src[0].IsTerminal())
            {
                if (char.ToLower(src[0]) == 'e')
                {
                    src = src[1..];
                    return ParseSciFloat(ref src, integer + ans);
                }
                if (!char.IsAsciiDigit(src[0]))
                    throw new ParseException();

                int digit = src[0] - '0';
                ans += digit * pos;
                pos *= 0.1;

                src = src[1..];
            }
            return Val(integer + ans);
        }

        private static Value ParseSciFloat(ref ReadOnlySpan<char> src, double mantissa)
        {
            int exp = 0;
            bool isNeg = false;
            while (src.Length > 0 && !src[0].IsTerminal())
            {
                if (src[0] == '-')
                {
                    isNeg = true;
                    src = src[1..];
                    continue;
                }

                if (!char.IsAsciiDigit(src[0]))
                    throw new ParseException();

                exp *= 10;
                exp += src[0] - '0';

                src = src[1..];
            }
            if (isNeg) exp = -exp;
            return Val(mantissa * double.Exp10(exp));
        }
        #endregion

        public static Value ParseString(ref ReadOnlySpan<char> src)
        {
            if (src[0] != '"') throw new ParseException();
            src = src[1..];

            StringBuilder builder = new();
            while (src.Length > 0 && src[0] != '"')
            {
                int p = 0;
                while (p < src.Length && src[p] != '\\' && src[p] != '"') ++p;
                builder.Append(src[..p]);

                if (src[p] == '\\')
                {
                    ++p;
                    builder.Append(src[p] switch
                    {
                        'n' => '\n',
                        'b' => '\b',
                        't' => '\t',
                        'r' => '\r',
                        '"' => '"',
                        '\\' => '\\',
                        _ => throw new ParseException(),
                    });
                    ++p;
                }
                src = src[p..];
            }
            src = src[1..];
            return Val(builder.ToString());
        }

        private static Ast ParseIdent(ref ReadOnlySpan<char> src)
        {
            int p = 0;
            while (p < src.Length && !src[p].IsTerminal() && src[p] != ':' && !src[p].IsCompoundBegin()) ++p;
            string ans = src[..p].ToString();
            src = src[p..];

            // ident( .. ) syntax sugar.
            if (src[0].IsCompoundBegin())
            {
                var args = ParseCompound(ref src);
                switch (args)
                {
                    case Value: 
                        return Comp(Id(ans));
                    case Compound c:
                        c.asts = [Id(ans), .. c.asts];
                        return c;
                };
            }

            // ident : value syntax sugar.
            p = 0;
            while (p < src.Length && char.IsWhiteSpace(src[p])) ++p;
            if (src[p] == ':')
            {
                src = src[(p + 1)..];
                var val = ParseAst(ref src);
                if (val is KVPair) throw new ParseException();
                return Kv(Id(ans), val);
            }
            return Id(ans);
        }

        private static KVPair ParseKVPair(ref ReadOnlySpan<char> src)
        {
            if (src[0] != ':') throw new ParseException();
            src = src[1..];

            var key = ParseAst(ref src);
            var value = ParseAst(ref src);

            if (key is KVPair)
                throw new ParseException($"{{KVPair}}'s key( {key} ) shouldn't be {{KVPair}}");
            if (value is KVPair)
                throw new ParseException($"{{KVPair}}'s value( {value} ) shouldn't be {{KVPair}}");

            return Kv(key, value);
        }
    }

    public static class AstParserCharExt
    {
        public static bool IsCompoundBegin(this char ch) => ch switch
        {
            '(' or '[' or '{' => true,
            _ => false,
        };

        public static bool IsCompoundEnd(this char end, char begin) => begin switch
        {
            '(' => end == ')',
            '[' => end == ']',
            '{' => end == '}',
            _ => false,
        };

        public static bool IsAnyCompoundEnd(this char ch) => ch switch
        {
            ')' or ']' or '}' => true,
            _ => false,
        };

        public static bool IsTerminal(this char ch) => char.IsWhiteSpace(ch) || ch.IsAnyCompoundEnd();
    }
}